package org.zhaya.sdk.processors;

import io.logz.sawmill.*;
import io.logz.sawmill.annotations.ProcessorProvider;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.zhaya.sdk.util.JsonProcessorsUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static java.util.Objects.requireNonNull;

@ProcessorProvider(type = "json", factory = JsonObjectaAndStringProcessor.Factory.class)
@Slf4j
public class JsonObjectaAndStringProcessor implements Processor {

    private final String field;
    private final Template targetField;
    private final String theWay;

    public JsonObjectaAndStringProcessor(String field, Template targetField, String theWay) {
        this.field = requireNonNull(field, "field cannot be null");
        this.targetField = targetField;
        this.theWay = checkTheWay(theWay);
    }

    public static String checkTheWay(String theWay) {
        // 如果为null则默认为string转object(STRINGTOOBJECT),兼容以前数据、版本
        if (theWay == null) {
            theWay = "STRINGTOOBJECT";
        } else {
            theWay = theWay.toUpperCase();
            boolean checkFlag = false;
            for (TheWays way : TheWays.values()) {
                if (theWay.equals(way.name())) {
                    checkFlag = true;
                    break;
                }
            }
            if (!checkFlag) {
                throw new NullPointerException("theWay not found, theWay that can be selected [OBJECTFLATTEN,STRINGTOOBJECT]");
            }
        }
        return theWay;
    }

    @Override
    public ProcessResult process(Doc doc) {
        // json拉平
        if (theWay.equals(TheWays.OBJECTFLATTEN.name())) {
            if (!doc.hasField(field)) {
                return ProcessResult.failure(String.format("failed to split field in path [%s]", field));
            }
            Object fieldObject = doc.getField(field);
            if (null != fieldObject) {
                try {
                    if (targetField == null) {
                        if (fieldObject instanceof Map) {
                            Map<String, Object> jsonMap = (Map<String, Object>) fieldObject;
                            jsonMap.forEach(doc::addField);
                        } else {
                            List<String> pathElements = tokenizePath(field);
                            String leafKey = pathElements.get(pathElements.size() - 1);
                            doc.addField(leafKey, fieldObject);
                        }
                    } else {
                        doc.addField(targetField.render(doc), fieldObject);
                    }
                } catch (Exception e) {
                    return ProcessResult.failure(String.format("Can not cast to JSONObject, field is [%s], msg=[%s]",
                            fieldObject, e.getMessage()));
                }
            }
        } else if (theWay.equals(TheWays.STRINGTOOBJECT.name())) {
            // json串转object
            if (!doc.hasField(field, String.class)) {
                return ProcessResult.failure(String.format("failed to parse json, couldn't find field [%s] or not instance of [%s]", field, String.class));
            }

            Map<String, Object> jsonMap = null;
            List<Map<String, Object>> listMap = null;
            String jsonString = doc.getField(field);
            boolean isList;
            try {
                isList = JsonProcessorsUtils.isList(jsonString);
                if (isList) {
                    listMap = JsonProcessorsUtils.fromJsonString(List.class, jsonString);
                } else {
                    jsonMap = JsonProcessorsUtils.fromJsonString(Map.class, jsonString);
                }
            } catch (RuntimeException e) {
                log.trace("failed to parse json {} error {}", jsonString, e.getMessage(), e);
                doc.appendList("tags", "_jsonparsefailure");
                return ProcessResult.failure(String.format("failed to parse json [%s]", jsonString));
            }

            if (targetField != null) {
                doc.addField(targetField.render(doc), isList ? listMap : jsonMap);
            } else {
                if (isList) {
                    for (Map<String, Object> map : listMap) {
                        map.forEach(doc::addField);
                    }
                } else {
                    jsonMap.forEach(doc::addField);
                }
            }
        }
        return ProcessResult.success();
    }

    public static class Factory implements Processor.Factory {

        private final TemplateService templateService;

        public Factory() {
            this.templateService = new TemplateService();
        }

        @Override
        public Processor create(Map<String, Object> config) {

            Configuration configuration = JsonProcessorsUtils.fromJsonMap(Configuration.class, config);
            Template targetField = StringUtils.isEmpty(configuration.getTargetField()) ? null : templateService.createTemplate(configuration.getTargetField());
            return new JsonObjectaAndStringProcessor(configuration.getField(), targetField, configuration.getTheWay());
        }
    }

    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Configuration implements Processor.Configuration {
        private String field;
        private String targetField;
        private String theWay;

    }

    public enum TheWays {
        OBJECTFLATTEN,
        STRINGTOOBJECT
    }

    private static List<String> tokenizePath(String s) {
        List<String> pathTokens = new ArrayList<>();
        StringBuilder sb = new StringBuilder(s.length());
        boolean inEscape = false;

        for (char c : s.toCharArray()) {
            if (inEscape) {
                inEscape = false;
                sb.append(c);
            } else if (c == '\\') {
                inEscape = true;
            } else if (c == '.') {
                pathTokens.add(sb.toString());
                sb.setLength(0);
            } else {
                sb.append(c);
            }
        }
        pathTokens.add(sb.toString());

        return pathTokens;
    }
}